package ex.learnflink;

import ex.datatypes.TaxiRide;
import ex.util.generator.TaxiRideGenerator;
import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.api.common.JobExecutionResult;
import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.state.ValueState;
import org.apache.flink.api.common.state.ValueStateDescriptor;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.KeyedProcessFunction;
import org.apache.flink.streaming.api.functions.sink.PrintSinkFunction;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.util.Collector;

import java.time.Duration;

/**
 *“长车程警报”练习的目标是对于持续超过两个小时的出租车车程发出警报。
 */

public class LongRidesSolution {

    private final SourceFunction<TaxiRide> source;
    private final SinkFunction<Long> sink;

    /**
     * Creates a job using the source and sink provided.
     */
    public LongRidesSolution(SourceFunction<TaxiRide> source, SinkFunction<Long> sink) {

        this.source = source;
        this.sink = sink;
    }

    /**
     * Creates and executes the long rides pipeline.创建并执行长途运输管道。
     * @return {JobExecutionResult}
     * @throws Exception which occurs during job execution.
     */
    public JobExecutionResult execute() throws Exception {

        // set up streaming execution environment
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        //开始生成数据
        DataStream<TaxiRide> rides = env.addSource(source);

        // WatermarkStrategy指定了如何提取时间戳和生成水印
        WatermarkStrategy<TaxiRide> watermarkStrategy =
                WatermarkStrategy.<TaxiRide>forBoundedOutOfOrderness(Duration.ofSeconds(60))
                        .withTimestampAssigner(
                                (ride, streamRecordTimestamp) -> ride.getEventTimeMillis());

        // create the pipeline
        rides.assignTimestampsAndWatermarks(watermarkStrategy)
                .keyBy(ride -> ride.rideId)
                .process(new AlertFunction())
                .addSink(sink);

        // execute the pipeline and return the result
        return env.execute("Long Taxi Rides");
    }

    /**
     * Main method.
     *
     * @throws Exception which occurs during job execution.
     */
    public static void main(String[] args) throws Exception {

        LongRidesSolution job =
                new LongRidesSolution(new TaxiRideGenerator(), new PrintSinkFunction<>());

        job.execute();
    }
//    行  718: START:每次车程的唯一id：1539,START,事件的时间戳：2020-01-01T20:33:00Z,-73.69795,40.975327,-73.827255,40.810764,3,每一辆出租车的唯一id:2013000179,每一位司机的唯一id:2013000179
//    行 1476: END:  每次车程的唯一id：1539,END,事件的时间戳：2020-01-01T22:36:00Z,-73.69795,40.975327,-73.827255,40.810764,3,每一辆出租车的唯一id:2013000179,每一位司机的唯一id:2013000179
//    行 1477: 2> 1539
//
//    行  734: START:每次车程的唯一id：1541,START,事件的时间戳：2020-01-01T20:33:40Z,-73.69758,40.975807,-73.86726,40.759842,3,每一辆出租车的唯一id:2013000132,每一位司机的唯一id:2013000132
//    行 1478: END: 每次车程的唯一id：1541,END,事件的时间戳：2020-01-01T22:36:40Z,-73.69758,40.975807,-73.86726,40.759842,3,每一辆出租车的唯一id:2013000132,每一位司机的唯一id:2013000132
//    行 1479: 2> 1541

    @VisibleForTesting
    public static class AlertFunction extends KeyedProcessFunction<Long, TaxiRide, Long> {

        private ValueState<TaxiRide> rideState;

        @Override
        public void open(Configuration config) {
            ValueStateDescriptor<TaxiRide> rideStateDescriptor =
                    new ValueStateDescriptor<>("ride event", TaxiRide.class);
            rideState = getRuntimeContext().getState(rideStateDescriptor);
        }

        @Override
        public void processElement(TaxiRide ride, Context context, Collector<Long> out)
                throws Exception {

            TaxiRide firstRideEvent = rideState.value();

            //车辆还没记录
            if (firstRideEvent == null) {
                // 记录车程
                rideState.update(ride);
                // 输出车程信息
                System.out.println("START:"+ride.toString());
                if (ride.isStart) {
                    //注册2小时计时器
                    context.timerService().registerEventTimeTimer(getTimerTime(ride));
                }
            //车辆已记录
            } else {
                //isStart  : Boolean,行程开始事件为 TRUE， 行程结束事件为 FALSE
                //start时间，超过2小时
                if (ride.isStart) {
                    if (rideTooLong(ride, firstRideEvent)) {
                        System.out.println("START:大于2小时的车程，"+ride.toString());
                        out.collect(ride.rideId);
                    }
                } else {
                    // end事件，超过2小时， 删除定时器
                    context.timerService().deleteEventTimeTimer(getTimerTime(firstRideEvent));
                    if (rideTooLong(firstRideEvent, ride)) {
                        System.out.println("END:"+ride.toString());
                        out.collect(ride.rideId);
                    }
                }
                //现在两个事件都被看到了，我们可以清除状态
                //如果缺少事件，此解决方案可能会泄漏状态
                //见DISCUSSION。获取更多信息
                rideState.clear();
            }
        }

        @Override
        public void onTimer(long timestamp, OnTimerContext context, Collector<Long> out)
                throws Exception {

            //计时器只在飞行时间太长时才会触发
            out.collect(rideState.value().rideId);

            // 清除现在可以防止重复警报，但如果END到达，则会泄漏状态
            rideState.clear();
        }

        private boolean rideTooLong(TaxiRide startEvent, TaxiRide endEvent) {
            return Duration.between(startEvent.eventTime, endEvent.eventTime)
                    .compareTo(Duration.ofHours(2))
                    > 0;
        }

        private long getTimerTime(TaxiRide ride) throws RuntimeException {
            if (ride.isStart) {
                return ride.eventTime.plusSeconds(120 * 60).toEpochMilli();
            } else {
                throw new RuntimeException("Can not get start time from END event.");
            }
        }
    }
}
